﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Diagnostics;

namespace NesGfx
{
    static class Utils
    {
        public static void CopyStream(Stream output, Stream input)
        {
            byte[] buffer = new byte[8 * 1024];
            while (true)
            {
                int read = input.Read(buffer, 0, buffer.Length);
                if (read <= 0)
                    return;
                output.Write(buffer, 0, read);
            }
        }
        public static bool compare(byte[] p, params string[] sArr)
        {
            int offset = 0;
            return compare(p, ref offset, sArr);
        }
        public static bool compare(byte[] p, ref int offset, params string[] sArr)
        {
            foreach (var s in sArr)
            {
                foreach (var c in s)
                {
                    if (p[offset++] != c)
                        return false;
                }
            }
            return true;
        }

        public static byte[] Splice(this byte[] src, int offset, int length)
        {
            var dst = new byte[length];
            Buffer.BlockCopy(src, offset, dst, 0, length);
            return dst;
        }
        
    }

    public class Rom
    {
        public byte[] rom = null;
        public int prgCount, chrCount, mapperNum;
        public bool fourScreenVaram, usingTrainer, sram, vMirror;
        public byte[] header = new byte[16], trainer = null, prg = null, chr = null, footer = null;


        public byte this[int address] { get { return rom[address]; } set { rom[address] = value; } }

        //public int prgrom { get { return prg; } }
        //public int chrrom { get { return chr; } }
        void load(MemoryStream m)
        {
            load(m.GetBuffer().Splice(0, (int)m.Length));
        }
        void load(byte[] r_)
        {
            rom = r_;
            var r = rom;
            if (!(Utils.compare(r, "NE") && ((r[2] == 'S') || r[2] == 'Z') && r[3] == 0x1A))
            {
                throw new Exception("Invalid NES rom. (Expects iNes format)");
            }
            prgCount = r[4];
            chrCount = r[5];
            fourScreenVaram = (r[6] & 8) > 0;
            usingTrainer = (r[6] & 4) > 0;
            sram = (r[6] & 2) > 0;
            vMirror = (r[6] & 1) > 0;
            mapperNum = r[6] >> 4;
            mapperNum |= r[7] & (15 << 4);


            Buffer.BlockCopy(r, 0, header, 0, 16);
            if (usingTrainer)
                trainer = r.Splice(16, 512);

            int offset = usingTrainer ? 512 + 16 : 16; //Fixed another bug :p (Swapped around values.) - Matrixz
            int size = prgCount * 16 * 1024;
            prg = r.Splice(offset, size);

            offset += size;
            size = chrCount * 8 * 1024;
            if (size > 0)
                chr = r.Splice(offset, size);

            offset += size;
            size = r.Length - offset;
            if (size > 0)
                footer=r.Splice(offset, size);
        }
        public Rom(string filename)
        {
            var f = new FileStream(filename, FileMode.Open, FileAccess.Read);
            var ms = new MemoryStream();
            Utils.CopyStream(ms, f);
            //load(ms.GetBuffer());
            load(ms);
            ms.Close();
            f.Close();
        }

        public void Save(string fn)
        {
            using (var fs = File.Open(fn, FileMode.OpenOrCreate, FileAccess.Write))
            {
                fs.Write(rom, 0, rom.Length);
            }
        }

        public Tile chrromAsTile()
        {
            var r = new Tile();
            r.set(chr);
            return r;
        }
    }

    public class Render
    {
        Bitmap bitmap;
        public float mplX, mplY;
        public Tile tile = new Tile();
        readonly int tileWidth = 8, tileHeight = 8;
        public Palette pal = new Palette();

        public Render(Bitmap bitmapOut_, float mutiply)
        {
            bitmap = bitmapOut_;
            mplX = mutiply;
            mplY = mutiply;
        }
        public Render(Byte[] chrrom, Bitmap bitmapOut_, float mutiply)
        {
            tile.set(chrrom);
            bitmap = bitmapOut_;
            mplX = mutiply;
            mplY = mutiply;
        }
        public Render(Byte[] chrrom, Bitmap bitmapOut_, float multipleX, float multipleY)
        {
            tile.set(chrrom);
            bitmap = bitmapOut_;
            mplX = multipleX;
            mplY = multipleY;
        }

        public void Draw(TSA.TSAElement elem, int offsetX_, int offsetY_)
        {
            Draw(elem, offsetX_, offsetY_, null);
        }
        public void Draw(TSA.TSAElement elem, int offsetX_, int offsetY_, int[] map)
        {
            lockIt();
            for (uint r = 0; r < elem.r; r++)
                for (uint c = 0; c < elem.c; c++)
                {
                    var tile = elem[r, c];
                    if (map != null) tile = map[tile];
                    Draw(tile, (int)(offsetX_ + (c * tileWidth * mplX)), (int)(offsetY_ + (r * tileHeight * mplY)));
                }
            unlockIt();
        }

        System.Drawing.Imaging.BitmapData bmData;
        int lockCount = 0, stride;
        public void lockIt()
        {
            if (lockCount++ == 0)
            {
                bmData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    System.Drawing.Imaging.ImageLockMode.WriteOnly,
                    System.Drawing.Imaging.PixelFormat.Format32bppArgb);
                stride = bmData.Stride;
            }
        }
        public void unlockIt()
        {
            if (--lockCount == 0)
            {
                bitmap.UnlockBits(bmData);
            }
        }

        public void Draw(int tileNumber, int offsetX_, int offsetY_)
        {
            tile.set(tileNumber);
            int offsetX = offsetX_;
            int offsetY = offsetY_;
            var b = new uint[8 * 8];

            //thanks to tile, this looks simple
            for (int y = 0; y < 8; y++)
            {
                for (int x = 0; x < 8; x++)
                {
                    b[y * 8 + x] = pal[tile[y * 8 + x]];
                }
            }

            lockIt();
            unsafe
            {
                byte* p = (byte*)(void*)bmData.Scan0;
                for (int y = 0; y < 8; y++)
                {
                    for (int x = 0; x < 8; x++)
                    {
                        for (int y2 = (int)(y * mplY); y2 < (y + 1) * mplY; ++y2)
                        {
                            for (int x2 = (int)(x * mplX); x2 < (x + 1) * mplX; ++x2)
                            {
                                uint* px = (uint*)&p[stride * (offsetY + y2)];
                                px[offsetX + x2] = pal[tile[y * 8 + x]];
                            }
                        }
                    }
                }
            }
            unlockIt();
        }

    }

    public class Palette
    {
        public uint[] rgbPalette = new uint[4];

        // & 0x3F to ignore the higher bits like the nes does
        public void setPalette(byte[] buf, int addr)
        {
            rgbPalette[0] = palette[buf[addr+0] & 0x3F];
            rgbPalette[1] = palette[buf[addr + 1] & 0x3F];
            rgbPalette[2] = palette[buf[addr + 2] & 0x3F];
            rgbPalette[3] = palette[buf[addr + 3] & 0x3F];
        }
        public void setPalette(int a, int b, int c, int d)
        {
            rgbPalette[0] = palette[a & 0x3F];
            rgbPalette[1] = palette[b & 0x3F];
            rgbPalette[2] = palette[c & 0x3F];
            rgbPalette[3] = palette[d & 0x3F];
        }
        public Palette()
        {
            rgbPalette[0] = (uint)Color.White.ToArgb();
            rgbPalette[1] = (uint)Color.Red.ToArgb();
            rgbPalette[2] = (uint)Color.Green.ToArgb();
            rgbPalette[3] = (uint)Color.Blue.ToArgb();
        }
        //public int pal[int i] { get { return i*2; } }
        public uint[] palette = defaultPalette;
        public readonly static uint[] defaultPalette =
        {
        0xFF525252, 0xFF000080, 0xFF08008A, 0xFF2C007E, 0xFF4A004E, 0xFF500006, 0xFF440000, 0xFF260800, 
        0xFF0A2000, 0xFF002E00, 0xFF003200, 0xFF00260A, 0xFF001C48, 0xFF000000, 0xFF000000, 0xFF000000, 

        0xFFA4A4A4, 0xFF0038CE, 0xFF3416EC, 0xFF5E04DC, 0xFF8C00B0, 0xFF9A004C, 0xFF901800, 0xFF703600, 
        0xFF4C5400, 0xFF0E6C00, 0xFF007400, 0xFF006C2C, 0xFF005E84, 0xFF000000, 0xFF000000, 0xFF000000, 

        0xFFFFFFFF, 0xFF4C9CFF, 0xFF7C78FF, 0xFFA664FF, 0xFFDA5AFF, 0xFFF054C0, 0xFFF06A56, 0xFFD68610,
        0xFFBAA400, 0xFF76C000, 0xFF46CC1A, 0xFF2EC866, 0xFF34C2BE, 0xFF3A3A3A, 0xFF000000, 0xFF000000, 

        0xFFFFFFFF, 0xFFB6DAFF, 0xFFC8CAFF, 0xFFDAC2FF, 0xFFF0BEFF, 0xFFFCBCEE, 0xFFFAC2C0, 0xFFF2CCA2,
        0xFFE6DA92, 0xFFCCE68E, 0xFFB8EEA2, 0xFFAEEABE, 0xFFAEE8E2, 0xFFB0B0B0, 0xFF000000, 0xFF000000
        };

        public uint this[uint i]
        {
            get
            {
                if (i > 3)
                    Debug.Assert(false, "Must be 0-3");
                return rgbPalette[i];
            }
        }
    }

    public class Tile
    {
        Byte[] buf = null;
        int offset = 0;
        public void set(Byte[] buffer) { buf = buffer; }
        public void set(int tileNumber) { offset = tileNumber * 16; }

        //I know someone will complain that this isnt optimize and will say
        //i should do 8 pixels at a time while i have the 2 bytes in memory.
        //but this is C#, its to tiny of a gain and if you want performance go to C++
        //Also a better optimization would to convert these into an array and return
        //the 0-3 value from it instead.

        uint bit0(uint v) { return (v > 0u) ? 1u : 0; }
        uint bit1(uint v) { return (v > 0u) ? 2u : 0; }

        public uint this[int px] //px from 0 to 8x8
        {
            get
            {
                uint bitMask = (uint)(1 << (7 - (px & 7)));
                uint o = (uint)(px / 8);
                return
                    bit0(buf[offset + o] & bitMask) +
                    bit1(buf[offset + o + 8] & bitMask);
            }
            set
            {
                uint bitMask = (uint)(1 << (7 - (px & 7)));
                uint o = (uint)(px / 8);
                byte b = (byte)((buf[offset + o] & (~bitMask)) + ((value & 1) << (7 - (px & 7))));
                buf[offset + o] = b;
                b = (byte)((buf[offset + o + 8] & (~bitMask)) + (((value & 2) >> 1) << (7 - (px & 7))));
                buf[offset + o + 8] = b;
            }
        }
        public uint this[int y, int x] //px from 0 to 8x8
        {
            get
            {
                return this[y * 8 + x];
            }
            set
            {
                this[y * 8 + x] = value;
            }
        }
    }

    public class TSA
    {
        byte[] buf;
        uint offset, TSA_stride, column_stride;
        public uint amount, c, r;
        void ctor(byte[] buf_, uint offset_, uint amount_, uint c_, uint r_)
        {
            buf = buf_;
            offset = offset_;
            amount = amount_;
            c = c_;
            r = r_;
            setStride(c * r, c);
        }
        public TSA(byte[] buf, uint offset, uint amount, uint c, uint r) { ctor(buf, offset, amount, c, r); }
        public TSA(byte[] buf, uint offset, uint amount, uint columnAndRow) { ctor(buf, offset, amount, columnAndRow, columnAndRow); }
        public TSA(byte[] buf, uint c, uint r) { ctor(buf, 0, uint.MaxValue, c, r); }
        public TSA(byte[] buf, uint columnAndRow) { ctor(buf, 0, uint.MaxValue, columnAndRow, columnAndRow); }

        void setStride(uint TSA_stride_, uint column_stride_)
        {
            TSA_stride = TSA_stride_;
            column_stride = column_stride_;
        }

        public int get(uint i, uint r_, uint c_)
        {
            //i should change these asserts to exceptions
            Debug.Assert(i < amount);
            Debug.Assert(c_ < c);
            Debug.Assert(r_ < r);
            return buf[offset + (TSA_stride * i) + r_ * column_stride + c_];
        }
        public int this[uint i, uint r, uint c] { get { return get(i, r, c); } }
        public TSAElement this[uint i]
        {
            get
            {
                return new TSAElement(this, i);
            }
        }
        public TSAElement this[int i]
        {
            get
            {
                return new TSAElement(this, (uint)i);
            }
        }
        public class TSAElement
        {
            public TSA parent;
            public uint i;
            public TSAElement(TSA p, uint index) { parent = p; i = index; }

            public uint c { get { return parent.c; } }
            public uint r { get { return parent.r; } }

            public int this[uint r, uint c]
            {
                get
                {
                    return parent.get(i, r, c);
                }
            }
        }
    }
}


